#pragma once 

#include "core/Common.hpp"

class LineRenderer {

public:

	LineRenderer() : nLineStorage(4000), vao(0), vbo(0) {
			
		glGenBuffers(1, &vbo);
		glGenVertexArrays(1, &vao);

		glBindBuffer(GL_ARRAY_BUFFER, vbo);
		{
			glBufferData(GL_ARRAY_BUFFER, sizeof(PC3DVertex) * nLineStorage, nullptr, GL_DYNAMIC_DRAW);
		} glBindBuffer(GL_ARRAY_BUFFER, 0);

		glBindVertexArray(vao);
		{
			glBindBuffer(GL_ARRAY_BUFFER, vbo);
			{
				unsigned int offset = 0;
				glVertexAttribPointer(semantic::attr::POSITION, 3, GL_FLOAT, GL_FALSE, sizeof(PC3DVertex), BUFFER_OFFSET(offset));
				offset += sizeof(glm::vec3);
				glVertexAttribPointer(semantic::attr::COLOR, 4, GL_FLOAT, GL_FALSE, sizeof(PC3DVertex), BUFFER_OFFSET(offset));
			} 
			glEnableVertexAttribArray(semantic::attr::POSITION);
			glEnableVertexAttribArray(semantic::attr::COLOR);
		} glBindVertexArray(0);
	}

	void add(glm::vec3 & startPos, glm::vec3 & endPos, const glm::vec4 & color) {
		vertices.push_back(PC3DVertex(startPos, color));
		vertices.push_back(PC3DVertex(endPos, color));
	}
	void draw(const glm::mat4& viewMatrix) {
			
		if (vertices.size() < 2)
			return;

		if (vertices.size() > nLineStorage) {
			throw std::runtime_error("add growing vbo to line render");
		}

		// upload all lines added this frame 

		glBindBuffer(GL_ARRAY_BUFFER, vbo);
		{
			glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(PC3DVertex) * vertices.size(), &vertices[0]);
		} glBindBuffer(GL_ARRAY_BUFFER, 0);

		// draw lines
		Shader* defaultShader = Game::getInstance()->simpleShader_pc;
		defaultShader->begin();
		{
			int viewLoc = defaultShader->getUniLoc("view");
			int projLoc = defaultShader->getUniLoc("proj");
			int modelLoc = defaultShader->getUniLoc("model"); 

			glm::mat4 modelMatrix = glm::mat4(1.0);
			glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(modelMatrix));
			glUniformMatrix4fv(viewLoc, 1, GL_FALSE, glm::value_ptr(viewMatrix));
			glUniformMatrix4fv(projLoc, 1, GL_FALSE, glm::value_ptr(Game::getPerspMatrix()));

			glBindVertexArray(vao);
			{
				glDrawArrays(GL_LINES, 0, vertices.size());
			} glBindVertexArray(0);
		}
		defaultShader->end();

		::flushGLError("LineRenderer::draw()");

		vertices.clear();
	}

private:
	GLuint vao;
	GLuint vbo;

	unsigned int nLineStorage;
	std::vector<PC3DVertex> vertices;
};

class PointRenderer {

public:

	PointRenderer() : nPointStorage(100000), vao(0), vbo(0) {

		glGenBuffers(1, &vbo);
		glGenVertexArrays(1, &vao);

		glBindBuffer(GL_ARRAY_BUFFER, vbo);
		{
			glBufferData(GL_ARRAY_BUFFER, sizeof(PC3DVertex) * nPointStorage, nullptr, GL_DYNAMIC_DRAW);
		} glBindBuffer(GL_ARRAY_BUFFER, 0);

		glBindVertexArray(vao);
		{
			glBindBuffer(GL_ARRAY_BUFFER, vbo);
			{
				unsigned int offset = 0;
				glVertexAttribPointer(semantic::attr::POSITION, 3, GL_FLOAT, GL_FALSE, sizeof(PC3DVertex), BUFFER_OFFSET(offset));
				offset += sizeof(glm::vec3);
				glVertexAttribPointer(semantic::attr::COLOR, 4, GL_FLOAT, GL_FALSE, sizeof(PC3DVertex), BUFFER_OFFSET(offset));
			} 
			glEnableVertexAttribArray(semantic::attr::POSITION);
			glEnableVertexAttribArray(semantic::attr::COLOR);
		} glBindVertexArray(0);
	}

	void add(glm::vec3 & point, const glm::vec4 & color) {
		vertices.push_back(PC3DVertex(point, color));
	}
	void add(const std::vector<PC3DVertex> & points) {
		vertices.insert(vertices.end(), points.begin(), points.end());
	}

	void draw(const glm::mat4& viewMatrix) {

		if (vertices.empty())
			return;

		if (vertices.size() > nPointStorage) {
			throw std::runtime_error("add growing vbo to point render");
		}

		// upload all lines added this frame 

		glBindBuffer(GL_ARRAY_BUFFER, vbo);
		{
			glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(PC3DVertex) * vertices.size(), &vertices[0]);
		} glBindBuffer(GL_ARRAY_BUFFER, 0);

		// draw lines
		Shader* defaultShader = Game::getInstance()->simpleShader_pc;
		defaultShader->begin();
		{
			int viewLoc = defaultShader->getUniLoc("view");
			int projLoc = defaultShader->getUniLoc("proj");
			int modelLoc = defaultShader->getUniLoc("model"); 

			glm::mat4 modelMatrix = glm::mat4(1.0);
			glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(modelMatrix));
			glUniformMatrix4fv(viewLoc, 1, GL_FALSE, glm::value_ptr(viewMatrix));
			glUniformMatrix4fv(projLoc, 1, GL_FALSE, glm::value_ptr(Game::getPerspMatrix()));

			glBindVertexArray(vao);
			{
				glDrawArrays(GL_POINTS, 0, vertices.size());
			} glBindVertexArray(0);
		}
		defaultShader->end();

		::flushGLError("LineRenderer::draw()");

		vertices.clear();
	}

private:
	GLuint vao;
	GLuint vbo;

	unsigned int nPointStorage;
	std::vector<PC3DVertex> vertices;
};



class BoxPrimitive {

public:

	struct InstanceDef {

		InstanceDef() :
			mat(0), color(0)
		{}
		InstanceDef(const glm::mat4 & mat, const glm::vec4 & color) :
			mat(mat), color(color)
		{}

		glm::mat4 mat;
		glm::vec4 color;
	};

	BoxPrimitive () : vao(0), vbo(0), instBuf(0) {

		createColoredBox(1.0f, 1.0f, 1.0f, vertices);

		glGenBuffers(1, &vbo);
		glGenVertexArrays(1, &vao);
		glGenBuffers(1, &instBuf);

		glBindBuffer(GL_ARRAY_BUFFER, vbo);
		{
			glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(P3DVertex), &vertices[0], GL_STATIC_DRAW);
		} glBindBuffer(GL_ARRAY_BUFFER, 0);

		glBindVertexArray(vao);
		{
			glBindBuffer(GL_ARRAY_BUFFER, instBuf);

			const int modelLoc = 1; // todo fix
			const int colorLoc = 5; // todo fix
			int offset = 0;

			for (unsigned int i = 0; i < 4 ; i++) {
				glEnableVertexAttribArray(modelLoc + i);
				glVertexAttribPointer(modelLoc + i, 4, GL_FLOAT, GL_FALSE, sizeof(InstanceDef),
					(const GLvoid*)(sizeof(GLfloat) * i * 4));
				glVertexAttribDivisor(modelLoc + i, 1);
			}
			offset += sizeof(glm::mat4);

			glEnableVertexAttribArray(colorLoc);
			glVertexAttribPointer(colorLoc, 4, GL_FLOAT, GL_FALSE, sizeof(InstanceDef), BUFFER_OFFSET(offset));
			glVertexAttribDivisor(colorLoc, 1);

			glBindBuffer(GL_ARRAY_BUFFER, vbo);
			{
				unsigned int offset = 0;
				glVertexAttribPointer(semantic::attr::POSITION, 3, GL_FLOAT, GL_FALSE, sizeof(P3DVertex), BUFFER_OFFSET(offset));
			} 
			glEnableVertexAttribArray(semantic::attr::POSITION);
		} glBindVertexArray(0);
	}

	void drawBatched(const glm::mat4& viewMatrix) {

		if (instanceDefList.empty())
			return;

		Shader* instancedShader = Game::getInstance()->simpleShader_pc_instanced;
		instancedShader->begin();
		{
			int viewLoc = instancedShader->getUniLoc("view");
			int projLoc = instancedShader->getUniLoc("proj");

			// upload instance buffer
			glBindBuffer(GL_ARRAY_BUFFER, instBuf);
			glBufferData(GL_ARRAY_BUFFER, sizeof(InstanceDef) * instanceDefList.size(), &instanceDefList[0], GL_DYNAMIC_DRAW);

			//glm::mat4 modelMatrix = glm::translate(glm::mat4(1.0f), pos);
			//glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(modelMatrix));

			glUniformMatrix4fv(viewLoc, 1, GL_FALSE, glm::value_ptr(viewMatrix));
			glUniformMatrix4fv(projLoc, 1, GL_FALSE, glm::value_ptr(Game::getPerspMatrix()));

			glBindVertexArray(vao);
			{
				glDrawArraysInstanced(GL_QUADS, 0, vertices.size(), instanceDefList.size());
			} glBindVertexArray(0);

		} instancedShader->end();

		instanceDefList.clear(); // clear for rendering next frame 
	}

	void draw(const glm::vec3 & pos, const glm::vec3 & scale, glm::vec4 & color) {
		
		// push back instance info for later batched rendering 
		glm::mat4 modelMatrix = glm::scale(glm::translate(glm::mat4(1.0f), pos), scale);
		 
		instanceDefList.push_back(InstanceDef(modelMatrix, color));
	}

	void createColoredBox(float width, float height, float breadth, std::vector<P3DVertex>& vertices )
	{
		vertices.resize(4*6);

		width   *= 0.5f;
		height  *= 0.5f;
		breadth *= 0.5f;
		unsigned int n = 0;												   
		vertices[n++] = P3DVertex(glm::vec3(-width, -height, breadth));
		vertices[n++] = P3DVertex(glm::vec3(width, -height, breadth));
		vertices[n++] = P3DVertex(glm::vec3(width, height, breadth));
		vertices[n++] = P3DVertex(glm::vec3(-width, height, breadth));

		vertices[n++] = P3DVertex(glm::vec3(width, -height, breadth));
		vertices[n++] = P3DVertex(glm::vec3(width, -height, -breadth));
		vertices[n++] = P3DVertex(glm::vec3(width, height, -breadth));
		vertices[n++] = P3DVertex(glm::vec3(width, height, breadth));

		vertices[n++] = P3DVertex(glm::vec3(width, -height, -breadth));
		vertices[n++] = P3DVertex(glm::vec3(-width, -height, -breadth));
		vertices[n++] = P3DVertex(glm::vec3(-width, height, -breadth));
		vertices[n++] = P3DVertex(glm::vec3(width, height, -breadth));

		vertices[n++] = P3DVertex(glm::vec3(-width, -height, -breadth));
		vertices[n++] = P3DVertex(glm::vec3(-width, -height, breadth));
		vertices[n++] = P3DVertex(glm::vec3(-width, height, breadth));
		vertices[n++] = P3DVertex(glm::vec3(-width, height, -breadth));

		vertices[n++] = P3DVertex(glm::vec3(-width, height, breadth));
		vertices[n++] = P3DVertex(glm::vec3(width, height, breadth));
		vertices[n++] = P3DVertex(glm::vec3(width, height, -breadth));
		vertices[n++] = P3DVertex(glm::vec3(-width, height, -breadth));

		vertices[n++] = P3DVertex(glm::vec3(-width, -height, -breadth));
		vertices[n++] = P3DVertex(glm::vec3(width, -height, -breadth));
		vertices[n++] = P3DVertex(glm::vec3(width, -height, breadth));
		vertices[n++] = P3DVertex(glm::vec3(-width, -height, breadth));
	}

private:
	GLuint vao;
	GLuint vbo;
	GLuint instBuf;

	std::vector<P3DVertex> vertices;
	std::vector<InstanceDef> instanceDefList;
};
